import os, sys
import shutil
import copy
import re
import zipfile
import traceback
import time
from datetime import datetime

# import docx
from docx import Document
from docx.shared import Pt, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml.table import CT_Tbl
from docx.oxml.text.paragraph import CT_P
from docx.table import _Cell, Table
from docx.text.paragraph import Paragraph

try:
    import xml.etree.cElementTree as ET
except ImportError:
    import xml.etree.ElementTree as ET
import xlwt

import atexit
import platform
IS_WINDOWS = False
if platform.system()=="Windows":
    import win32process
    import win32api
    import win32com
    import win32com.client as win32 # 直接使用win32会报错
    import pythoncom
    from docx2pdf import convert
    IS_WINDOWS = True
import subprocess

import multiprocessing
import threading 
from pathlib import Path

from colorama import Fore, init
init(autoreset=True)

from hm.common import *
from hm.hos import *
from hm.xls import XLS
    

    
# +++++++++++++++++++ 备份文件夹 +++++++++++++++++++
MYDIR = os.path.dirname(__file__)
BACKUP_parent = os.path.join(MYDIR, 'backup')

def remove_old_back():
    old_dirs = os.listdir(BACKUP_parent)
    if len(old_dirs)>20:
        old_dirs = sorted(old_dirs, key=lambda x:os.path.getmtime(os.path.join(BACKUP_parent, x)))
        for i,fol in enumerate(old_dirs):
            if i==10:
                break
            try: # 日期转换可能失败
                shutil.rmtree(os.path.join(BACKUP_parent, fol))
            except:
                pass

class BACKUP:
    # 即使是单例模式，多进程下面还是会执行多次，不能保证只创建一个备份文件夹
    instance = None
    def __new__(cls):
        if cls.instance is None:
            cls.instance = super().__new__(cls)
            # # 所以弄了个临时文件f，保证程序只运行一次下面的代码
            cls.temp_file = os.path.join(BACKUP_parent, 'temp')
            current_process = multiprocessing.current_process()
            if current_process.name=='MainProcess': 
                remove_old_back()    
                with open(cls.temp_file, 'w') as f:
                    name = time.strftime("%Y%m%d%H%M%S", time.localtime())
                    cls.dir = os.path.join(MYDIR, 'backup', name)
                    os.makedirs(cls.dir, exist_ok=True)
                    f.write(cls.dir)
                    print(f"{Fore.LIGHTBLACK_EX}😶备份文件夹见: {cls.dir}")
            else:
                with open(cls.temp_file, 'r') as f:
                    cls.dir = f.read()
        return cls.instance

# 备份目录
BACKUP_DIR = BACKUP().dir




# ++++++++++++++++++++ 日志对象 +++++++++++++++++++++++
def init_logger():
    path = os.path.join(MYDIR, "odbc.log")
    if multiprocessing.current_process().name=='MainProcess': 
        with open(path, 'w') as f: # 清空文件内容
            f.write('start log')
    return HmLog(path)

log = init_logger()
logger = log.logger


# ++++++++++++++++++++ win32com word对象 +++++++++++++++++++++++
# def fix_gen_py():
#     # 解决gen_py报错问题
#     # 方法一
#     user_path = os.path.expanduser('~')
#     genpy = os.path.join(user_path, 'AppData', 'Local', 'Temp', 'gen_py')
#     v = sys.version_info
#     py_path = os.path.join(genpy, f"{v[0]}.{v[1]}")
#     if os.path.exists(py_path):
#         for n in os.listdir(py_path):
#             path = os.path.join(py_path, n)
#             if os.path.isdir(path):
#                 if re.search(r'(\w{4}-){3}', n) is not None:
#                     shutil.rmtree(path)
#                     print(f"{Fore.GREEN} 移除gen_py{path}成功")
#     #方法二
#     # excel = win32.gencache.EnsureDispatch('Excel.Application') # 或Word
#     # #change to =>
#     # excel = win32.Dispatch('Excel.Application')
    
#     # 方法三
#     # print("处理genpy", win32.gencache.GetGeneratePath())  # 这将打印出缓存的路径)
#     # shutil.rmtree(os.path.join(os.environ.get('LOCALAPPDATA'), 'Temp', 'gen_py'))
    
    
# def close_word_by_force(word_app):
#     # Get the window's process id's
#     hwnd = word_app.Hwnd
#     t, p = win32process.GetWindowThreadProcessId(hwnd)
#     try:
#         handle = win32api.OpenProcess(win32com.PROCESS_TERMINATE, 0, p)
#         if handle:
#             win32api.TerminateProcess(handle, 0)
#             win32api.CloseHandle(handle)
#             print("关闭word成功")
#     except:
#         pass
#     # subprocess.run(["taskkill", "/F", "/IM", "WINWORD.EXE"])

@atexit.register
def quit_app():
    current_process = multiprocessing.current_process()
    if current_process.name=='MainProcess': # 防止多进程调用多次
        WordApp.Quit()


class WordApp:
    instance = None
    def __new__(cls):
        if cls.instance is None:
            for attempt in range(2):
                try:
                    # pythoncom.CoInitialize() # 多线程不加这个会报错
                    cls.app = win32.gencache.EnsureDispatch('Word.Application')
                    # cls.app = win32.Dispatch('Word.Application')
                    # cls.app = win32.dynamic.Dispatch('Word.Application')
                    # cls.app = win32.DispatchEx('Word.Application') # 使用独立进程打开
                    cls.app.Visible = False
                    cls.app.DisplayAlerts = 0
                    cls.instance = super().__new__(cls)
                except AttributeError as e:
                    if e.name == 'CLSIDToClassMap':
                        print("try to reopen word...wait", end='')
                        mod_name = e.obj.__name__
                        mod_name_parts = mod_name.split('.')
                        if len(mod_name_parts) == 3:
                            # Deleting the problematic module cache folder
                            folder_name = mod_name_parts[2]
                            gen_path = Path(win32com.__gen_path__)
                            folder_path = gen_path.joinpath(folder_name)
                            shutil.rmtree(folder_path)
                            # Deleting the reference to the module to force a rebuilding by gencache
                            del sys.modules[mod_name]
                            continue
                        raise Exception("There was an error loading Excel.") from e
        return cls.instance

    @classmethod
    def Quit(cls):
        if cls.instance:
            cls.app.Quit()
            del cls.app
            # pythoncom.CoUninitialize()
            print("Quit word application")


class WordDocument:
    def __init__(self, docpath=''):
        self.docpath = docpath
        self.app = WordApp().app
    
    def __enter__(self):
        if self.docpath:
            self.doc = self.app.Documents.Open(self.docpath)
        else:
            self.doc = self.app.Documents.Add()
        return self.doc
        
    def __exit__(self, *args): 
        self.doc.Close()
        

def merge_docx_abandoned(folder):
    '''
        python-docx合并多个docx内容（已弃用）
    '''
    paths = []
    for i in os.listdir(folder):
        if i.endswith(".docx") and not OSis_hidden(i):
            paths.append(OS.join(folder, i))
    # 创建一个新的Word文档
    print("start merging docx file")
    new_doc = Document()
    for path in paths:
        # 加载每个文档
        doc = Document(path)
        # 将每个文档的内容添加到新文档中
        for element in doc.element.body:
            new_doc.element.body.append(element)
    # 保存合并后的文档
    save_path = OSrep_name(paths[0], 'merge_output')
    new_doc.save(save_path)
    print(f"merged file has been saved in:{save_path}")
    
    
def merge_docx(folder):
    '''
        win32com合并多个docx文件内容为一个docx
    '''
    if not IS_WINDOWS:
        return merge_docx_abandoned(folder)
    # 获取文件夹下面的docx
    paths = []
    for i in os.listdir(folder):
        if i.endswith(".docx") and not OSis_hidden(i):
            paths.append(OS.join(folder, i))
    # 创建副本，防止对原文件造成任何改动
    copy_paths = []
    for i, docx_path in enumerate(paths):
        print(f"{i}、copy and auto_numbet_to_text {docx_path}")
        copy_path = OSrep_name(docx_path, OSname(docx_path)+"-copy")
        shutil.copy2(docx_path, copy_path)
        auto_number_to_text(copy_path)
        copy_paths.append(copy_path)
    save_path = OSrep_name(paths[0], 'merge_output')
    if save_path in copy_paths:
        copy_paths.remove(save_path)
    
    print("start merging docx file")
    # 更新20250317
    with WordDocument() as output_docx:
        output_content = output_docx.Content
        for i, docx_path in enumerate(copy_paths): 
            output_content.Collapse(0) # 相当于文件指针seek到末尾
            output_content.InsertFile(docx_path)
            output_content = output_docx.Content # 没有这一行，插入顺序将是反的
            print(f"{i}、merge {docx_path}")
        
        # 清理副本
        for i, docx_path in enumerate(copy_paths): 
            print(f"{i}、remove {docx_path}")
            os.remove(docx_path)

        # 保存输出
        output_docx.SaveAs(save_path)
        print(f"merged file has been saved in:{save_path}")
        output_docx.Close()


def to_pdf(docx_path):
    '''
        转换docx对象为pdf
    '''
    pdf_path = os.path.join(os.path.dirname(docx_path), OSname(docx_path)+".pdf")
    if os.path.exists(pdf_path):
        yes_no = input(f'文件{pdf_path}已存在，是否覆盖？Y/n')
        if yes_no not in ["Y",'y']:
            return pdf_path
    f = open(pdf_path, 'w')
    f.close()
    convert(docx_path, pdf_path)
    print(f'{docx_path}转换为pdf成功')
    time.sleep(1)
    return pdf_path


# 获取word文档总页数
def get_page_count_by_xml(docx_path):
    '''
        从文档元数据 获取页码，仅支持新版(office2019以上？)docx文档
    '''
    docx_object = zipfile.ZipFile(docx_path)
    docx_property_file_data = docx_object.read('docProps/app.xml').decode()
    page_count = re.search(r"<Pages>(\d+)</Pages>", docx_property_file_data)
    if page_count:
        page_count = int(page_count.group(1))
    # print("warnning: get page from the word medata maybe not correct!")
    return page_count


def get_page_count_by_win32(docx_path):
    '''
        以word应用对象获取页码
    '''
    # 新版本20250317
    if not IS_WINDOWS:
        return get_page_count_by_xml(docx_path)
    with WordDocument(docx_path) as doc:
        doc.Repaginate() 
        pages = doc.ComputeStatistics(2)
        # doc.Close(SaveChanges=0)
        return int(pages) # __exit__方法同样会被调用
    # 方法一 这种方法只有在Visible=True时能正确读取，而且经常失败
    # pages = doc.BuiltInDocumentProperties(14).Value 
    # properties = doc.BuiltInDocumentProperties # 效果同上面一行
    # pages = properties('Number of Pages').Value

    # 方法二 可能是先分页，后对页数进行了统计，但不清楚原理
    # WORD_APP.ActiveDocument.Repaginate()
    # pages = WORD_APP.ActiveDocument.ComputeStatistics(2)
  

def detect_auto_number(path):
    try:
        doc = Document(path)
        doc_xml = doc.element.xml
        root = ET.fromstring(doc_xml)
        # print(doc_xml.count("w:numPr"))
        # <w:ilvl w:val="0"/>
        # <w:numId w:val="1"/>
        w = "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}"
        if "w:numPr" in doc_xml:
            for node in root.iter(w+"numPr"):
                for child in node.iter(w+"numId"):
                    if child.attrib[w+'val'] != '0':
                        # print("文档含有自动编号")
                        return True 
    except Exception as e:
        logger.info(f"文档无法打开，无法读取自动编号:{path}, {str(e)}\n")
    return False
    
    

def auto_number_to_text(path):
    '''
    自动编号转常规文本
    '''
    if IS_WINDOWS:
        # 新版本20250317
        if detect_auto_number(path):
            # print(f"{Fore.LIGHTBLACK_EX}文档{os.path.basename(path)}含自动编号，正在识别...", end=" ")
            try:
                with WordDocument(path) as doc:
                    doc.ConvertNumbersToText() # todo 多进程模式下，此行代码会报错，呼叫word失败
                    doc.Save()
                    # print("ok 注意，文件会被保存为非自动编号\n")
            except Exception as e:
                # print("识别失败\n", str(e))
                logger.info(f"{path}自动编号读取失败")
                
        # for parg in doc.Paragraphs:
        #     parg.Range.ListFormat.ConvertNumbersToText()
        # for tb in doc.Tables:
        #     for i in range(1, tb.Rows.Count+1):
        #         for j in range(1, tb.Columns.Count+1):
        #             cell = tb.Cell(1, 1)
        #             if cell.Range.ListFormat.ListString:
        #                 cell.Range.ListFormat.ConvertNumbersToText()
        #                 print("**********************", cell.Range.ListFormat.ListString)
        #     # tb.Range.ListFormat.ConvertNumbersToText()
        #     # print("**********************", tb.Range.ListFormat.ListString)
        

def auto_number_to_text_many(paths):
    # import time
    # start = time.time()
    # ts = []
    print(f"{Fore.LIGHTBLACK_EX}😶正在读取含自动编号的文件", end='')
    for path in paths:
        print(f"{Fore.LIGHTBLACK_EX}.", end='')
        filename = os.path.basename(path)
        copy_path = os.path.join(BACKUP_DIR, filename)
        if not os.path.exists(copy_path): 
            shutil.copy2(path, copy_path)
        auto_number_to_text(path)
    print(f"{Fore.LIGHTBLACK_EX}注意：文件会保存为非自动编号")
    #     t = threading.Thread(target=auto_number_to_text, args=(path, ))
    #     t.setDaemon(True)
    #     t.start()
    #     ts.append(t)
    # for t in ts:
    #     t.join()
    # print("耗时", time.time()-start)
    
       

class HmDocx:
    def __init__(self, path):
        self.path = path
        self.filename = os.path.basename(path)
        try:
            self.copy_path = os.path.join(BACKUP_DIR, self.filename)
            if not os.path.exists(self.copy_path): 
                # user could use HmDocx for same file many times
                # so we only backup at first time, because the second time the file probably changed.
                shutil.copy2(self.path, self.copy_path)
       
            # auto_number_to_text(path)
            self.doc = Document(path)
            self.tables = self.doc.tables
            self._doc_string = ''
            self._empty_cell = []
            # 检查文件是否开启修订模式
            self.is_revised = False
            self.detect_revised()
            # self.original_doc_xml = copy.deepcopy(self.doc.element.xml)
            # self.PAGEs = get_page_count_by_xml(self.path)
            self.PAGEs = 0
        except Exception as e:
            logger.warning("不是docx文件"+traceback.format_exc())
            raise AssertionError("不是docx文件", self.path)
    
    def detect_revised(self):
        '''
            检查是否有修订内容，有的话，接受所有修订
        '''
        # 这一步比较耗时，但是为了检测中文和正确读取修订内容又不得不这么做
        for p in self.doc.paragraphs:
            self._doc_string += self.accept_all(p)
        for i,t in enumerate(self.tables):
            for j,row in enumerate(t.rows):
                try:
                    for k,cell in enumerate(row.cells):
                        for p in cell.paragraphs:
                            self._doc_string += self.accept_all(p)
                        # if not cell.text:
                        #     # 检测空的单元格，其实与这个函数无关
                        #     self._empty_cell.append([i, j, k])
                        # else:
                        #     if "electronic signature" in cell.text:
                        #         # 修改签名，其实与这个函数无关
                        #         self.replace_para_content("electronic signature", "wet signature", cell, font_rgb=RGBColor(0x00, 0x00, 0x00))
                except:
                    pass

    def accept_all(self, p):
        """
            Return text of a paragraph after accepting all changes
            '''
                # 插入的内容
                <w:ins w:id="6" w:author="海梦 蓝" w:date="2024-03-23T09:55:00Z">
                    <w:r>
                    <w:rPr>
                        <w:rFonts w:hint="eastAsia"/>
                    </w:rPr>
                    <w:t>我是修订内容</w:t>
                    </w:r>
                </w:ins>
                # 删除的内容
                <w:del w:id="5" w:author="海梦 蓝" w:date="2024-03-23T10:30:00Z">
                    <w:r w:rsidDel="00646464">
                    <w:delText>Parameters Test</w:delText>
                    </w:r>
                </w:del>
            '''
        """
        xml = p._p.xml
        if "w:del" in xml or "w:ins" in xml:
            self.is_revised = True
            tree = ET.fromstring(p._p.xml)
            # xml命名空间
            word_body = "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}"
            text_tag  = word_body + "t" # 修订的内容
            ins_tag = word_body+"ins"
            del_tag   = word_body + "delText"
            p.text = ''.join([node.text for node in tree.iter(text_tag) if node.text])
        text = p.text
        if "\xa0" in text:
            p.text = text.replace("\xa0", " ")
        return p.text


    # def reject_all(self, p):
    #     """
    #         Return text of a paragraph after rejecting all changes
    #     """
    #     word_body = "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}"
    #     text_tag  = word_body + "t"
    #     del_tag   = word_body + "delText"
    #     ins_tag   = word_body + "ins"
    #     xml = p._p.xml
    #     if "w:del" in xml or "w:ins" in xml:
    #         tree = XML(xml)
    #         # find and remove all insert tags
    #         for insert in tree.findall(ins_tag):
    #             tree.remove(insert)
    #         # find all deletion tags and change to text tags
    #         for deletion in tree.getiterator(del_tag):
    #             deletion.tag = text_tag
    #         runs = (node.text for node in tree.getiterator(text_tag) if node.text)
    #         return "".join(runs)
    #     else:
    #         return p.text
    
    def iter_block_items(self, parent):
        """
        按顺序遍历doc文档的段落和表格，返回段落或者表格对象
        Generate a reference to each paragraph and table child within *parent*,
        in document order. Each returned value is an instance of either Table or
        Paragraph. *parent* would most commonly be a reference to a main
        Document object, but also works for a _Cell object, which itself can
        contain paragraphs and tables.
        用法见下面的cp_tables
        """
        import docx
        if isinstance(parent, docx.document.Document):
            parent_elm = parent.element.body
            # print(parent_elm.xml)
        elif isinstance(parent, _Cell):
            parent_elm = parent._tc
        else:
            raise ValueError("something's not right")

        for child in parent_elm.iterchildren():
            if isinstance(child, CT_P):
                yield Paragraph(child, parent)
            elif isinstance(child, CT_Tbl):
                yield Table(child, parent)
    
    def cp_table(self, table):
        '''
            复制一个table对象，并粘贴它下面
        '''
        # 获取该表格是文档中第几个表格
        target_tb_ix = self.tables.index(table) 
        # 获取该表格在文档中哪个段落（因为要利用 段落对象插入table的方法）
        p_ix, tb_ix = 0, 0
        for i,x in enumerate(self.iter_block_items(self.doc)):
            if isinstance(x, Paragraph):
                p_ix += 1
            if isinstance(x, Table):
                if tb_ix==target_tb_ix:
                    break
                tb_ix += 1
        # 在该表格后面的段落插入新表格
        # todo：如果该表格位于连续的几个表格之间，则新表格插入位置可能不对
        # todo：p_ix+1可能超出self.doc.paragraphs索引
        if p_ix:
            p = self.doc.paragraphs[p_ix+1] # 再往下一段
            p.insert_paragraph_before() # 插入空行
            p = self.doc.paragraphs[p_ix+1] # 在插入的空行处插入表格
            t = copy.deepcopy(table._tbl) # 复制表格内容
            p._p.addnext(t) # 插入表格
            t = Table(t, self.doc) # 创建表格容器
            self.tables.insert(tb_ix+1, t) 
            return t

    def cp_table_to_para(self, table, p):
        '''
            copy a table to the behind of paragraph
        '''
        new_table = copy.deepcopy(table)
        p._p.addnext(new_table._element)
        # tofo self.tables.insert(tb_ix+1, t) 
        return new_table


    def cp_run_style(self, source, new, **kwargs):
        '''
            source和new都是run对象
            复制source的格式给new
        '''
        new.style = source.style
        new.font.name = source.font.name
        new.font.bold = source.font.bold
        new.font.italic = source.font.italic
        new.font.size = source.font.size
        new.font.color.rgb = source.font.color.rgb

    def cp_para_style(self, source, new, **kwargs):
        '''
        复制段落source（的第一个run）格式给new段落
        '''
        # 修改新段落style
        new.style = source.style
        alignment = {"left":WD_ALIGN_PARAGRAPH.LEFT,
            "right":WD_ALIGN_PARAGRAPH.RIGHT
        }
        # 修改段落对齐
        if "alignment" in kwargs:
            # 用户自定义样式
            new.paragraph_format.alignment = alignment[kwargs["alignment"]]
        else:
            new.paragraph_format.alignment = source.paragraph_format.alignment
        
        if not new.runs: # 段落不一定有run(单元格似乎一定有段落)    
            # print("+++++++++++++++++++++add new run")
            new.add_run() # 新增空的run（下次插入内容会自动应用它的格式）
            
        for new_run in new.runs:
            # 默认样式
            new_run.font.name = "Times New Roman"
            new_run.font.bold = False
            new_run.font.italic = False
            new_run.font.size = Pt(10.5) # 五号
            new_run.font.color.rgb = RGBColor(0x00,0x00,0xFF) # 默认蓝色字体
            # 要复制的样式
            if source.runs:
                self.cp_run_style(source.runs[0], new_run, **kwargs)
            # 用户自定义样式
            if "font_rgb" in kwargs:
                new_run.font.color.rgb = kwargs.pop("font_rgb")
            for key in kwargs: # todo指明name, bold, italic, size
                setattr(new_run.font, key, kwargs[key])
        
    def clear(self, cell):
        '''
            清除单元格内容
        '''
        for p in cell.paragraphs:
            p.clear()
    
    def insert_paragraph(self, index, content, cell, clear=False, **kwargs):
        '''
            功能：在[单元格cell or doc]对象中插入新内容，并保持原有段落的格式
            index: 此段落（插入单元格之后）相对其他段落的索引值：
                   假设单元格原本有9个段落，index=9，那么在原本的第9个段落前插入新段落，原本的第9个段落变成第10个
                   index还支持负索引，假设原本有9个段落，index=-1表示在第9个段落之后插入，新段落变成第10个段落
            content: 插入新段落内容
            cell: 可以是docx表格的单元格对象，也可以是其他支持添加段落的对象
            clear: 插入前是否清空[单元格]
        '''
        len_p = len(cell.paragraphs)
        # 1. 处理索引
        # 处理后的index范围是 [0, len_p]
        if index<0: # 转换负索引为正索引
            index = len_p+index+1 
        if index<0:
            index=0 # 最小在第1个插入
        if index>len_p:
            index = len_p # 最大在最后一个插入
        # 2. 拷贝目标位置原来的样式
        source_p = None
        if len_p:
            # 优先使用插入位置前面一个段落的样式
            # (如果index=0，则index-1为最后一个段落的样式)
            source_p = copy.deepcopy(cell.paragraphs[index-1]) # 先拷贝，防止下面if clear被清空
        # 3. 是否先清空原本内容
        if clear:
            # todo 清除之后source_p就没了
            self.clear(cell)
        # 4. 插入新段落内容
        if clear or not len_p: # 直接赋值
            cell.text = content
            p = cell.paragraphs[0]
        elif index<len_p: # 在原来某个段落前面插入
            cell.paragraphs[index].insert_paragraph_before(content)
            p = cell.paragraphs[index]
        else: # 添加到最后一段
            cell.add_paragraph(content)
            p = cell.paragraphs[-1]
        # 5. 修改新段落样式
        if source_p:
            kwargs["font_rgb"] = RGBColor(0x00,0x00,0xFF) # 蓝色
            self.cp_para_style(source_p, p, **kwargs)
    
    def replace_para_content(self, old, new, cell, **kwargs):
        '''
            替换单元格中某一段的部分或全部内容，并标注为蓝色
            old：旧内容
            new：新内容
        '''
        # 要替换的内容在某个段落内
        for i, p in enumerate(cell.paragraphs):
            source_p = copy.deepcopy(p)
            matchs = [i for i in re.finditer(re.escape(old), source_p.text)]
            if matchs:
                p.clear()
                new_runs = []
                start = 0
                for match in matchs:  
                    p.add_run(source_p.text[start:match.start()])
                    run = p.add_run(new, style=source_p.runs[0].style)
                    new_runs.append(run)
                    start = match.end()
                if start<len(source_p.text):
                    p.add_run(source_p.text[start:])
                self.cp_para_style(source_p, p)
                for run in new_runs:
                    if "font_rgb" not in kwargs:
                        run.font.color.rgb = RGBColor(0x00, 0x00, 0xFF)  
                    else:
                        run.font.color.rgb = kwargs["font_rgb"]

    def delete_para_content(self, content, cell):
        '''
            删除单元格中某一段的部分内容
        '''
        self.replace_para_content(content, '', cell) 

    def save(self):
        self.doc.save(self.path)
            

class HmTable(HmDocx):
    ATTR = {}
    def __init__(self, t, parent=None):
        self.table = t
        self.rows = t.rows
        self.parent = parent # 用这个类（HmTable）创建实例的类
        self.init_ATTR()

    def init_ATTR(self):
        '''
            通过ATTR，将表格对象转化为 类属性，然后重写__setattr__方法实现通过操作属性来改变表格内容
            可参考index_DTR.py的IndexHeaderTbl用法：
            ATTR 的键是类属性名，值是对应表格的行列位置，以及该单元格原本的的内容（通过regex匹配）
                ATTR = {
                    "platform":{"row":1, "col":1, 'regex':r' *Platform: *(.+?)\nGUI'},
                    "GUI":{"row":1, "col":1, 'regex':r'GUI: *(.+?)(MACO)|.$'},
                    "date":{"row":0, "col":4},
                    "compiler":{"row":1, "col":4}
                }
            1. 如果row和col都存在，regex不存在，则直接匹配row和col对应的单元格，否则按regex匹配单元格
            2. 如果'row_offset'或'col_offset'存在，则在1匹配结果基础上偏移行和列
            3. 有正则，无子组
                    读取值如果有正则匹配到的内容，则读取移除匹配内容后的值
                    写入值，正则匹配的内容保留
                    比如 Tester: haimeng，正则表达式为Tester: ，
                        1）Tester:和haimeng不在同一个单元格，读取写入都操作haimeng
                        2) Tester:和haimeng为同一个单元格，读取时去掉Tester: , 写入是加上Tester
            4. 有正则子组, 
                1）读取，假设platform单元格原本内容为：
                VITROS LVCC 450 Chemistry System Environmental Test Plan and Procedure
                Platform:
                        45001020
                        45001025
                GUI:  
                        v1.0.0.125
                        v1.0.0.126
                        V1.0.0.128
            按照正则 只匹配到 45001020 和 45001025（子组），则只读取子组
                2）写入，如果操作self.platform = '45001038', 那么上面的单元格内容， 
            45001020 和 45001025 将被替换为'45001038' （只修改子组匹配的内容）
            5. 子组和offset不能同时设定，因为1）偏移是在正则找到的单元格a之后转移到单元格b，2）而子组要操作的是正则找到的单元格a
               程序无法判断到底要操作单元格a还是b()
            6. 正则如果匹配不到内容，读取值为None，并无法写入值
        '''
        for key in self.ATTR:
            x = self.locate_cell(**self.ATTR[key])
            value = ''
            if x:
                cell = self.rows[x[0]].cells[x[1]]
                value = cell.text
                if x[2]:
                    if x[2].groups():
                        value = x[2].group(1) 
                    else:
                        # 有正则，没有子组，value若包含匹配的内容，要移除掉
                        value = value.replace(x[2].group(), '')
                value = value.strip().strip("\n")
            setattr(self, key, value)
    
    def __setattr__(self, key, value):
        '''
            与init_ATTR方法搭配，通过操作属性来操作单元格
            key: 这个类的属性
            value: 要设置的单元格的内容
        '''
        # 有此属性且内容不一样再修改(避免init_ATTR死循环)
        if key in self.ATTR and hasattr(self, key) and getattr(self, key)!=value: 
            x = self.locate_cell(**self.ATTR[key])
            if x:
                cell = self.rows[x[0]].cells[x[1]]
                content = value
                if x[2]:
                    if x[2].groups():
                        content = cell.text[:x[2].start(1)] + value + cell.text[x[2].end(1):] # value仅替换第一个子组匹配的值
                    # 有正则，没有子组，目标单元格如果包含匹配的内容(Tester:和haimeng为同一个单元格)，要加上
                    elif x[2].group() in cell.text:
                        content = x[2].group()+value
                self.set_content(content, cell)
        super().__setattr__(key, value)
    
    def locate_cell(self, regex=None, flags=None, row=None, col=None, row_offset=0, col_offset=0):
        '''
        根据传入的参数，查找匹配的单元格
        '''
        match = None
        if regex:
            if not flags:
                flags = re.I # 默认不区分大小写
            if isinstance(row, int):
                rows = [self.rows[row]]
                i_start = row
            else:
                rows = self.rows
                i_start = 0
            for i,r in enumerate(rows):
                cells = r.cells
                j_start = 0
                if isinstance(col, int) and col<len(cells):
                    cells = [r.cells[col]]
                    j_start = col
                for j,c in enumerate(cells):
                    # 20250402 原本是如果找到匹配的值就停止查找，现在改成取最后一个匹配的值，
                    # 因为有可能匹配到的列是合并的单元格，接着offset_col后，还是它自己
                    match_tmp = re.compile(regex, flags=flags).search(c.text)
                    if match_tmp:
                        match = match_tmp
                        row = i_start+i
                        col = j_start+j
                        # break  
                if match:
                    break
        if row is not None and col is not None:
            return [row+row_offset, col+col_offset, match]

    def is_merge(self, cell1, cell2, direction='x'):
        '''
            判断两个单元格cell1和cell2是否为合并的单元格
        '''
        if direction=='x' and cell1._tc.right == cell2._tc.right:
            return True
        if direction=='y' and cell1._tc.bottom == cell2._tc.bottom:
            return True
        return False

    
    def cp_row_fmt(self, source, new, **kwargs):
        '''
            复制source行格式到new行
        '''
        source_cells = source.cells
        source_cells_len = len(source_cells)
        for i,s_c in enumerate(source_cells):
            n_c = new.cells[i]
            # 1. 依照source合并单元格
            # print(s_c.text, s_c._tc.top, s_c._tc.bottom, s_c._tc.left, s_c._tc.right)
            if i+1 < source_cells_len -1:
                # if s_c._tc.right==source.cells[i+1]._tc.right: 
                if self.is_merge(s_c, source_cells[i+1]): # 需要合并(当前单元格和下一个单元格左右列数一样)
                    n_c.merge(new.cells[i+1])
            # 2. 依照source设置段落
            source_p = s_c.paragraphs[0]
            new_p = n_c.paragraphs[0]
            # 因为如果new_P没有run对象，字体样式无法拷贝
            if not new_p.runs:
                new_p.add_run()
            self.cp_para_style(source_p, new_p, **kwargs)

    def insert_row(self, ix, cp_pre_fmt=True):
        '''
            在ix插入新行
            cp_pre_fmt: 复制前一行（如果有）的格式
        '''
        if ix<0:
            ix = len(self.rows)+ix+1 # ix=-1表示添加到最后一行
        if ix<len(self.rows): # 中间插入行
            tbl = self.table._tbl
            successor = tbl.tr_lst[ix]
            tr = tbl._new_tr()
            for gridCol in tbl.tblGrid.gridCol_lst:
                tc = tr.add_tc()
                tc.width = gridCol.w
            successor.addprevious(tr)
            row = self.rows[ix]
        else:
            row = self.table.add_row() # 末尾添加行
        if cp_pre_fmt:
            source = self.rows[ix-1] if ix>0 else self.rows[0]
            self.cp_row_fmt(source, row)
        return row

    def delete_row(self, row):
        '''
            删除行
        '''
        if isinstance(row, int):
            row = self.rows[row]
        row._element.getparent().remove(row._element) 

    def set_content(self, content, cell):
        '''
            设置单元格内容
        '''
        self.insert_paragraph(0, content, cell, clear=True)
        


# if __name__=="__main__":
#     path = r"E:\工作\VTSU\QST-ODBC-M2-VTSU-R0004 LVCC VersaTip Supply（VTSU）Subsystem Test Report.docx"
#     doc = HmDocx(path)
#     print("页码", doc.PAGEs)